<?php
namespace wechat;
use think\Request;
use RuntimeException;
use DOMDocument;
/**
 *  @file : MchApi.php
 *  @brief: 微信商户号相关接口
 *  微信企业付款 https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2
 *  JSAPI支付    https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
 */
class MchApi {

  const URL_API = 'https://api.mch.weixin.qq.com';
  
  protected $appid    = ''; // 申请商户号的appid或商户号绑定的appid
  protected $mchid    = ''; // 微信支付分配的商户号
  protected $mchkey   = ''; // 商户密钥, 参与sign运算
  protected $sslkey   = '';
  protected $sslcert  = '';
  
  public function __construct($opts) {
    $this->appid    = $opts['AppID'] ?? '';
    $this->mchid    = $opts['MchID'] ?? '';
    $this->mchkey   = $opts['MchKey'] ?? '';
    $this->sslkey   = $opts['SslKey'] ?? '';
    $this->sslcert  = $opts['SslCert'] ?? '';
  }
  /**
   *  @brief  : 提现(企业付款)到零钱
   *  @return : array|RuntimeException
   *  @details: https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers
   */
  public function CashPocket($param) {
    
    $param = array_merge([
      'device_info'       => '',  // 可选, 设备号
      'partner_trade_no'  => '',  // 必须, 商户订单号, 需保持唯一性(只能是字母或者数字, 不能包含有符号)
      'openid'            => '',  // 必须, 用户openid
      'check_name'        => true,// 必须, 校验用户姓名选项, NO_CHECK=不校验真实姓名;FORCE_CHECK=强校验真实姓名
      're_user_name'      => '',  // 可选, 收款用户姓名
      'amount'            => 0,   // 必须, 付款金额, 单位为分
      'desc'              => '',  // 必须, 企业付款描述信息
      'spbill_create_ip'  => '',  // 必须, 用户端或者服务端的IP
    ], $param);
    
    $param['mch_appid'] = $this->appid; // 必须, 商户账号appid
    $param['mchid']     = $this->mchid; // 必须, 商户号
    
    if (empty($param['device_info'])) {
      unset($param['device_info']);
    }
    if (empty($param['partner_trade_no'])) {
      $param['partner_trade_no'] = self::MakeTradeNo();
    }
    if (empty($param['openid'])) {
      throw new RuntimeException('Missing argument openid');
    }
    $param['check_name'] = $param['check_name'] ? 'FORCE_CHECK' : 'NO_CHECK';
    
    if (empty($param['re_user_name'])) {
      if ($param['check_name'] == 'FORCE_CHECK') {
        throw new RuntimeException('Missing argument re_user_name');
      } else {
        unset($param['re_user_name']);
      }
    }
    if ($param['amount'] <= 0) {
      throw new RuntimeException('Invalid argument amount');
    }
    if (empty($param['desc'])) {
      throw new RuntimeException('Missing argument desc');
    }
    if (empty($param['nonce_str'])) {
      $param['nonce_str'] = self::UUID(); // 必须, 随机字符串
    }
    if (empty($param['spbill_create_ip'])) {
      $param['spbill_create_ip'] = Request::instance()->ip();
    }
    $param['sign'] = self::MakeSign($param);
    $data = self::Array2XML($param);
    trace($data);

    $url = self::URL_API . '/mmpaymkttransfers/promotion/transfers';
    $eax = $this->_curl([
      CURLOPT_URL => $url,
      CURLOPT_POSTFIELDS  => $data,
    ]);
    trace($eax);
    if ($eax['errno'] != 0) {
      throw new RuntimeException($eax['error'], $eax['errno']); // curl执行错误
    }
    $eax = self::XML2Array($eax['data']);
    trace($eax);
    if (empty($eax)) {
      throw new RuntimeException('通讯失败');
    }
    if ($eax['return_code'] == 'FAIL') {
      throw new RuntimeException($eax['return_msg']); // 通讯失败的情况
    }
    return $eax;
  }

  /**
   *  @brief  : 根据`商户订单号`来查询`企业付款到零钱`交易情况
   *  @return : array
   *  @details: https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo
   */
  public function GetCashPocketTransferInfo($tradeNo) {
    
    $param = [
      'appid'     => $this->appid,
      'mch_id'    => $this->mchid,
      'nonce_str' => self::UUID(), // 必须, 随机字符串
      'partner_trade_no' => $tradeNo,
    ];
    $param['sign'] = self::MakeSign($param);
    $data = self::Array2XML($param);
    trace($data);

    $url = self::URL_API . '/mmpaymkttransfers/gettransferinfo';
    $eax = $this->_curl([
      CURLOPT_URL => $url,
      CURLOPT_POSTFIELDS  => $data,
    ]);
    trace($eax);
    if ($eax['errno'] != 0) {
      throw new RuntimeException($eax['error'], $eax['errno']); // curl执行错误
    }
    
    $eax = self::XML2Array($eax['data']);
    trace($eax);
    if (empty($eax)) {
      throw new RuntimeException('通讯失败');
    }
    if ($eax['return_code'] == 'FAIL') {
      throw new RuntimeException($eax['return_msg']); // 通讯失败的情况
    }
    
    return $eax;
  }
  /**
   *  @brief  : 提现(企业付款)到银行卡
   *  @return : array
   *  @details: https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank
   */
  public function CashBankCard() {
    
    
  }
  /**
   *  @brief  : 查询`企业付款到银行卡`交易情况
   *  @return : array
   *  @details: https://api.mch.weixin.qq.com/mmpaysptrans/query_bank
   */
  public function GetCashBankCardTransferInfo() {
    
    
  }
  /**
   *  @brief  : `Native`、`JSAPI`、`APP`统一下单
   *  @return : array|RuntimeException
   *  @details: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
   */
  public function UnifiedOrder($param) {
    
    $data = array_merge([ // 必填参数
      'appid'             => $this->appid,
      'mch_id'            => $this->mchid,
      'nonce_str'         => self::UUID(),
      'body'              => '',
      'out_trade_no'      => self::MakeTradeNo(),
      'total_fee'         => 0,  // 整数, 单位为分
      'spbill_create_ip'  => Request::instance()->ip(),
      'notify_url'        => '',
      'trade_type'        => '',
    ], $param);

    $data['sign'] = self::MakeSign($data);
    $data = self::Array2XML($data);
    trace($data);
    $url = self::URL_API . '/pay/unifiedorder';
    $eax = $this->_curl([
      CURLOPT_URL => $url,
      CURLOPT_POSTFIELDS  => $data,
    ]);
    trace($eax);
    if ($eax['errno'] != 0) {
      throw new RuntimeException($eax['error'], $eax['errno']); // curl执行错误
    }
    $eax = self::XML2Array($eax['data']);
    trace($eax);
    return $eax;
  }
  /**
   *  @brief  : curl内部封装
   *  @return : array
   *  @details:
   */
  protected function _curl($opts = []) {
    $ch = curl_init();
    $opts = $opts + [
      CURLOPT_ENCODING        => 'gzip, deflate',
      CURLOPT_RETURNTRANSFER  => true,
      CURLOPT_POST            => true,
      CURLOPT_SSL_VERIFYPEER  => false,
      CURLOPT_SSL_VERIFYHOST  => 2,
      // CURLOPT_SSLCERTPASSWD => $this->mchid,
      CURLOPT_SSLCERT       => $this->sslcert,
      CURLOPT_SSLKEY        => $this->sslkey,
    ];
    curl_setopt_array($ch, $opts);
    $data = curl_exec($ch);
    $retn = ['error' => curl_error($ch), 'errno' => curl_errno($ch), 'info' => curl_getinfo($ch), 'data' => $data];
    curl_close($ch);
    return $retn;
  }
  /**
   *  @brief  : 统一验签
   *  @param [in] $param : 欲验签数据, 字段值为空的将被移除
   *  @return : string
   *  @details: 
   */
  public function MakeSign(&$param) {
    $sign = '';
    if (isset($param['sign'])) {
      unset($param['sign']);
    }
    foreach($param as $key => $val) {
      if (empty($val)) {
        unset($param[$key]);
      }
    }
    ksort($param, SORT_STRING);
    // $stringA = http_build_query($param); // 不能直接http_build_query
    $stringA = [];
    foreach($param as $key => $val) {
      $stringA[] = "{$key}={$val}";
    }
    $stringA = implode('&', $stringA);
    $stringA = "{$stringA}&key={$this->mchkey}";
    $sign = strtoupper(md5($stringA));
    return $sign;
  }
  /**
   *  @brief  : 生成唯一订单号(固定18位)
   *  @return : string
   *  @details: 14位YmdHis + 3位随机数 + 1位校验码
   */
  public static function MakeTradeNo() {
    $retn = '';
    $xishu = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
    $yushu = [1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2]; // 余数0-10对应

    $retn = sprintf('%s%03d', date('YmdHis'), mt_rand(0, 999)); // 刚好17位
    $eax = 0;
    for ($i = 0; $i < 17; $i++) {
      $eax += $retn[$i] * $xishu[$i];
    }
    $retn = $retn.$yushu[$eax % 11];
    return $retn;
  }
  /**
   * 生成UUID 单机使用
   * @access public
   * @return string
   */
  public static function UUID() {
    $str = md5(uniqid(mt_rand(), true));
    return $str;
  }
  /**
   *  @brief  : xml字符串转array
   *  @param [in] $xml : 欲转换xml字符串
   *  @return : array
   *  @details: 
   */
  public static function XML2Array($xml) {
    $arr = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOBLANKS);
    $arr = json_encode($arr, JSON_UNESCAPED_UNICODE);
    $arr = json_decode($arr, true);
    return $arr;
  }
  /**
   *  @brief  : array转xml字符串, 只处理一级
   *  @param [in] $arr : 欲转换array数据
   *  @return : xml字符串
   *  @details: 
   */
  public static function Array2XML($arr) {
    $doc = new DOMDocument('1.0', 'UTF-8');
    $doc->formatOutput = true;
    $xml = $doc->appendChild($doc->createElement('xml'));
    foreach($arr as $key => $val) {
      $elm = $doc->createElement($key, $val);
      $xml->appendChild($elm);
    }
    $str = $doc->saveXML($xml);
    return $str;
  }
}
